/**
  @file: Settings.java

  PURPOSE: 
  Defines a single, global source for all program settings. Program settings
  are loaded either through file or from properties on construction. However
  also upon construction all program Resources are obtained.

  @author Bill Kraemer

  REVISION HISTORY ABSTRACT

  PROGRAMMER:  Bill Kraemer         ORG: MSE

  Copyright 2011 Mission Solutions LLC. All rights reserved.

  END ABSTRACT
 */
package com.mse.utilities;

import java.io.*;
import java.lang.StringBuilder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;
import java.util.Properties;

import com.mse.utilities.Resources;
import com.mse.utilities.SettingsKeys;
import com.mse.utilities.SettingsFileList;
import com.mse.utilities.UtilityException;

/**
  DESCRIPTION:
  This class defines a single, global source for all program settings.
 */
public class Settings {
   
   ///< singleton instance
   private static Settings instance = null;
   
   ///< wrapped properties instance
   private Properties properties = null;
   
   /**
     DESCRIPTION:
     Constructs an instance of Settings. This constructor is unused, but  
     defined privately to enforce the singleton pattern.
    */
   private Settings() {
   }
   
   /**
     DESCRIPTION:
     Constructs and configures an instance of Settings from a specified
     properties file.
   
     @param filename        name of properties file to load
     @throws IOException    to indicate file load failure
    */
   private Settings(String filename) throws IOException {
      InputStream is = new FileInputStream(filename);
      this.properties = new Properties();
      try {
         this.properties.load(is);
      } finally {
         is.close();
      }
   }

   /**
     DESCRIPTION:
     Constructs and configures an instance of Settings from a specified
     SettingsFileList. 

     @param sfl - SettingsFileList object of the filelist file to load
     @throws UtilityException to indicate file load failure
    */
   private Settings(SettingsFileList sfl) throws UtilityException {
      this.properties = new Properties();

      Map<String,String> props = sfl.getFileListSettings();

      for (String key : props.keySet()) {
         properties.put(key, props.get(key));
      }
   }
   
   /**
     DESCRIPTION:
     Constructs and configures an instance of Settings from a specified
     properties instance.
 
     @param properties properties instance to use
    */
   private Settings(Properties properties) {
      this.properties = properties;
   }
   
   /**
     DESCRIPTION:
     Load into settings all properties obtained from Resources.
 
     @throws UtilityException - if resource loading fails
    */
   private void establishResources() throws UtilityException {
      Resources ourRes = new Resources();

      this.properties.putAll(ourRes.getAsMap());
   }

   /**
     DESCRIPTION:
     This method establishes the value of the "Deploy_RootDir" property. This
     property value is passed as an environment variable by a script that is
     controlling main program execution.

      This method MUST be called after initialization of our "properties".

     @throws UtilityException if the environment has not be set
    */
   private void establishDeployRoot() throws UtilityException {
      this.properties.setProperty(SettingsKeys.Deploy_RootDir, 
                                  SettingsKeys.getDeployRoot());
   }

   /**
     DESCRIPTION:
     This method initializes Settings and creates its singleton instance. 
   
     @param filename name of properties file to load
     @throws UtilityException if initialization fails
    */
   public static void init(String filename) throws UtilityException {
      if (instance != null) return;
      try {
         instance = new Settings(filename);
         instance.establishDeployRoot();
         instance.establishResources();
      } catch(UtilityException ue) {
         instance = null; 
         throw new UtilityException("Initialization Failed:\n" +
                                    ue.getMessage());
      } catch(IOException io) {
         instance = null; 
         throw new UtilityException("Initialization Failed:\n" +
                                    io.getMessage());
      }
   }
   
   /**
     DESCRIPTION:
     This method initializes Settings and creates its singleton instance. 

     @param properties properties instance to use
     @throws UtilityException if initialization fails
    */
   public static void init(Properties properties) throws UtilityException {
      if (instance != null) return;
      try {
         instance = new Settings(properties);
         instance.establishDeployRoot();
         instance.establishResources();
      } catch(UtilityException ue) {
         instance = null; 
         throw new UtilityException("Initialization Failed:\n" +
                                    ue.getMessage());
      }
   }

   /**
     DESCRIPTION:
     This method initializes Settings and creates its singleton instance from
     a filelist.

     The lines of the filelist may reference a fully qualified path (start with
      '/'), or otherwise, be a relative path from the installation root
      directory. Any blank lines in the file are ignored, and any line with a
      '#' in the first column are comments, and ignored.
   
     @param fListPath - path to a filelist file to load
     @throws UtilityException if initialization fails
    */
   public static void initFromFileList(String fListPath)
   throws UtilityException {
      if (instance != null) return;
      try {
         SettingsFileList SFL = new SettingsFileList(fListPath);
         instance = new Settings(SFL);
         instance.establishDeployRoot();
         instance.establishResources();
      } catch(UtilityException ue) {
         instance = null; 
         throw new UtilityException("Initialization Failed:\n" +
                                    ue.getMessage());
      }
   }
   
   /**
     DESCRIPTION:
     This method returns the singleton instance. The init() method
     must be called before obtaining the instance.
   
     @return Settings instance
     @throws RuntimeException if instance is null (not initialized). 
    */
   public static Settings getInstance() {

      if (instance == null) {
         throw new RuntimeException("Settings instance is null");
      }

      return instance;
   }
   
   /**
     DESCRIPTION:
     This method returns a requested property value.
   
     @param key     property key string
     @return        property value string
    */
   public String getProperty(String key) {
      return this.properties.getProperty(key);
   }
   
   /**
     DESCRIPTION:
     This method returns a requested property value or a specified
     default value if the property key is not defined.
   
     @param key             property key string
     @param defaultValue    default value string
     @return                property value string
    */
   public String getProperty(String key, String defaultValue) {
      return this.properties.getProperty(key,defaultValue);
   }
   
   /**
     DESCRIPTION:
     This method sets a requested property value.

     @param key     property key string
     @param value   property value string
    */
   public void setProperty(String key, String value) {
      this.properties.setProperty(key,value);
   }

   /**
     DESCRIPTION:
     This method unsets (removes) a requested property definition.

     @param key     property key string
    */
   public void unsetProperty(String key) {
      this.properties.remove(key);
   }

   /**
     DESCRIPTION:
     This method returns a string of all keys and their values contained in
     this object.

     @return text - all key value pairs
    */
   public String toString() {
      StringBuilder       sb = new StringBuilder(2048);
      Enumeration<String> itor;
      ArrayList<String>   aList = new ArrayList<String>(50);

      sb.append("\n");

      itor = (Enumeration<String>) this.properties.propertyNames();

      // get keys in a list so we can sort them... ugh....
      while (itor.hasMoreElements()) {
         aList.add(itor.nextElement());
      }

      Collections.sort(aList);

      for (String key : aList) {
         sb.append(key);
         sb.append(" = ");
         sb.append(this.properties.getProperty(key));
         sb.append("\n");
      }

      return sb.toString();
   }
}
